#ifndef __SELECT_SERVER_H__
#define __SELECT_SERVER_H__

#include <sys/select.h>
#include <iostream>
#include "Log.hpp"
#include "Sock.hpp"

using namespace std;

#define BITS 8
#define NUM (sizeof(fd_set) * BITS) // 监测文件描述符的最多个数
#define FD_NONE -1 // FD_NONE表示该文件描述符不需要关心

// select只关心读取时间,不关心写入事件和异常事件
// select一开始只关心listensock,随着连接的增多,select关心的文件描述符会越来越多
// 如果看待listensock？listensock是用来获取新连接的，我们依旧把它看作IO事件,input事件
// 如果新连接没有到来，那么直接调用accept会被阻塞住，所以需要让select关心listensock的读事件
class SelectServer
{
public:
    SelectServer(const uint16_t& port = 8080)
        : _port(port)
    {
        _listenSock = Sock::Socket();
        Sock::Bind(_listenSock, _port); // 绑定IP地址和端口号
        Sock::Listen(_listenSock);  // 将_listenSock设置为监听套接字
        logMessage(DEBUG, "Create ListenSock Success!");
        // _fdArray数组中存储的都是要求select帮我们关心的文件描述符
        // FD_NONE为无效的文件描述符
        for(int i = 0; i < NUM; ++i) _fdArray[i] = FD_NONE;
        // 约定: _fdArray[0] = _listenSock
        _fdArray[0] = _listenSock;
    }

    void Start()
    {
        while(true)
        {
            Debug();

            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = _listenSock; // select需要关心的最大文件描述符
            // 设置select需要关心的文件描述符
            for(int i = 0; i < NUM; ++i)
            {
                // 无效的文件描述符直接跳过即可
                if(_fdArray[i] == FD_NONE) continue;
                else
                {
                    FD_SET(_fdArray[i], &rfds); // 让select关心_fdArray[i]的读事件
                    if(_fdArray[i] > maxfd) maxfd = _fdArray[i]; // 更新最大文件描述符
                }
            }

            // 前五秒阻塞等待,如果文件描述符的读事件还没有就绪,就从select中返回
            // struct timeval timeout = {5, 0};
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            // int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
            switch (n)
            {
            case -1:
                logMessage(WARNING, "Select Error %d %s", errno, strerror(errno));
                break;
            case 0:
                // 文件描述符的读事件未就绪,此时可以去处理其他事情
                logMessage(DEBUG, "Read Event Not Ready"); 
                break;
            default:
                // 读事件就绪
                logMessage(DEBUG, "Read Event Ready");
                // 读事件就绪时,就需要处理该事件,不然的话
                // 内核就会一直通知你读事件就绪了
                HandlerEvents(rfds);
                break;
            }
        }
    }

    ~SelectServer()
    {
        if(_listenSock >= 0) close(_listenSock);
    }

private:
    void HandlerEvents(const fd_set& rfds)
    {
        for(int i = 0; i < NUM; ++i)
        {
            // 跳过不合法的文件描述符
            if(_fdArray[i] == FD_NONE) continue;
            // 合法的文件描述符不一定就绪,在位图结构中的文件描述符才是就绪的
            if(FD_ISSET(_fdArray[i], &rfds))
            {
                // 1、读事件就绪,新连接到来,需要调用Accepter
                // 2、读事件就绪,数据到来,需要调用Recver
                if(_fdArray[i] == _listenSock)
                    Accepter(); 
                else
                    Recver(i);
            }
        }
    }

    void Accepter()
    {
        string clientIp;
        uint16_t clientPort = 0;
        // 此时获取新连接不会被阻塞住
        int sock = Sock::Accept(_listenSock, &clientIp, &clientPort);
        if(sock < 0)
        {
            logMessage(WARNING, "Accepter Error");
            return;
        }
        logMessage(NORMAL, "Accept A New Link Success: [%s:%d] fd:%d", clientIp.c_str(), clientPort, sock);
        
        // 找到一个合法的位置,将获取的新连接放到_fdArray数组中
        int pos = 1;
        for(; pos < NUM; ++pos)
        {
            if(_fdArray[pos] == FD_NONE) break;
        }

        // NUM就是select能够检测文件描述符数目的上限,如果pos
        // 等于NUM,则说明现在select检测的文件描述符已经有NUM个
        // 了,无法对新获取的连接进行监测了,所以只能将该连接关闭掉
        if(pos == NUM)
        {
            logMessage(WARNING, "SelectServer Already Full, Close: %d", sock);
        }
        else
            _fdArray[pos] = sock;
    }

    void Recver(int pos)
    {
        // 此时调用read/recv等函数就不会被阻塞住了
        logMessage(NORMAL, "Data Arrives on %d File Descriptor", _fdArray[pos]);
        char buffer[1024];
        // 这里是有bug的,因为使用TCP协议进行通信,没有进行协议定制
        // 无法保证此次读取的数据就是一个完整的报文,而不是多个报文或半个报文
        int n = read(_fdArray[pos], buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            logMessage(NORMAL, "Client[%d]# %s", _fdArray[pos], buffer);
        }
        else if(n == 0)
        {
            // 客户端关闭连接,服务端也要关闭连接
            logMessage(NORMAL, "Client[%d] Quit, Me Too");
            // 先关闭文件描述符,然后不再让select关心该文件描述符
            close(_fdArray[pos]);
            _fdArray[pos] = FD_NONE;
        }
        else
        {
            logMessage(WARNING, "%d File Descriptor Read Error %d:%s", _fdArray[pos], errno, strerror(errno));
            // 先关闭文件描述符,然后不再让select关心该文件描述符
            close(_fdArray[pos]);
            _fdArray[pos] = FD_NONE;
        }
    }

    // 打印select所关心的文件描述符
    void Debug()
    {
        cout << "_fd_array[]: ";
        for(int i = 0; i < NUM; i++)
        {
            if(_fdArray[i] == FD_NONE) continue;
            cout << _fdArray[i] << " ";
        }
        cout << endl;
    }

private:
    uint16_t _port;
    int _listenSock;
    // _fdArray数组中保存的是select需要关心的文件描述符
    int _fdArray[NUM];
};

#endif